【第1404期】使用小程序做交互的技巧
前言
世上没有绝对的公平,只是看成本。今日早读文章由轻芒@范怀宇分享。
正文从这开始~~
在这里跟大家分享一些轻芒做小程序的具体实践,结合我们在做轻芒杂志过程中的技术选择,重点和大家聊一些轻芒杂志交互实现的经验。
开始之前,先自我介绍一下,我大概 2009 年开始入行,2011 年去了豌豆荚,2016 年底我和王俊煜、崔瑾一起出来做了轻芒。在过去十年的职业生涯中,我经历过很多「端」,我们最早在 Windows 上提供豌豆荚的各种服务,然后还有塞班系统、Android、iOS 以及 Web,现在小程序也是非常重要的一个「端」。
看整个客户端的技术发展会发现,从「端」的技术来讲是越来越薄的。原来做 Windows 开发,你可能面临很多的光碟,非常厚的 MSDN 的文档,还有海量的 Windows32 API。而到小程序,已经越来越薄,需要了解的细节也更少。这是因为,平台上的框架会变得越来越厚,我们开发者入手一个平台会变得越来越简单。今天「端」的发展趋势是,用户在哪里,「端」就在哪里,我们更多的是来适应「端」,而且无论在哪个「端」都需要提供最好的产品体验。
轻芒在小程序发布的第一天就上线了和小程序相关的产品,包括为用户推荐高品质内容的轻芒杂志,我们也把产品经验开放给内容创作者使用,通过轻芒小程序+,他们无需代码就可以搭建自己的小程序。
回到今天的主题:交互。首先要搞清楚什么是交互,在传统客户端的分层逻辑里我们会常听到 MVC,数据、控制、界面,这是一个可能用了二十几年的分层模型。交互毫无疑问包含内容呈现,把我们需要给用户的数据呈现出来。还有一部分很重要的,是你要和用户互动,交互界面通常不是静态页面,用户会在界面上产生一些行为,这些行为通过控制层会反馈到数据层,数据层进行一些变化和更迭,再通过控制层送回给界面层重新进行数据的渲染和呈现,这是整个 MVC 流转的方式。
除了内容的呈现和渲染,还有很重要的一点是要处理好所有和用户的互动,这块其实占了前端很大的开发量,因此,会有的人可能前端开发无聊,有的人则会觉得非常有魅力。做交互其实没有什么银弹。在软件开发里我们一直会需要一个「银弹」,就是「当我知道那个秘籍之后,我就可以做得非常轻松,什么事情都可以变得非常潇洒」,其实并不存在。大量的实践混杂在细节里,包括我今天和大家分享的很多东西,听上去没有那么神奇,甚至说有的一点都不工程,但这是真正的实践。你真正要把一些东西做好,很多都是在细节里面的。
首先,我先介绍一下小程序的平台特点,因为所有的前端开发,都是在一个平台上进行,你得对这个平台非常熟悉,就像在一个舞台上做演出,你需要知道舞台在哪里、光在哪里、声音在哪里等等。你需要了解这个平台的特性,才能在后面的实践中更加自如。
小程序交互层的设计特点
跟我经历过的其他平台相比,先我觉得小程序是个非常纯粹的数据驱动的前端平台。比如,现在去用 Web 或者 Android 开发,如果你需要在界面点一个按钮来隐藏一个地方,或者点一个按钮来展开某个地方,你会怎么做?按刚刚 MVC 模型,你可能不会穿过 Controller 去到 Model,然后改了 Model 再回来,不会走这个路径。你肯定会在前端用一些简单的脚本或技巧,让这个东西隐藏起来、再展开,这样不会影响核心数据模型,而且很便利。
但在整个小程序里其实没有这一块东西,你不能操作任何 DOM 节点,这意味着不管你做什么事情,包括改变界面的任何一点点状态,都需要通过控制层传到逻辑层,用 js 经过一些计算,再调用 setData 函数,触发对 Data 变化的比对,重新对界面进行一次渲染,这是小程序整个的内核驱动模型。这个模型坦白讲有很多部分非常重,会限制你做一些特别灵活的实现,但对我们开发者来讲,只有理解这个模型,才会知道如果去做开发,哪些环节需要特别注意。
还有一个大家非常熟悉的点,小程序是传统的 Web 组件混着一些原生组件,就是用本地语言,比如说 iOS 上的 Objective C 或者是 Android 上 Java 来实现的一个本地组件,这和 Web 上面实现的组件是不一样的。小程序采取的技术路线,是所谓的分层渲染,底下是 Web 层,上面是原生层,当我滑动 Web 层时,原生层会接到一些事件,它会尝试跟 Web 层联动,但它们其实不是一起动的,中间有一个延迟,这个延迟会带来很多麻烦事情,尤其是由于这个分层是原生层永远活在 Web 层上面,这也会给交互设计、交互实践带来一些麻烦。
还有一个特色是单窗口,当然在移动时代其他平台也如此,在一个交互页面,我们只会跟一个窗口打交道。小程序特别的一点是它彻底取消了窗口这个概念,可能我们都感觉不到窗口的存在。如果大家做过 Android,就知道 Android 可以做悬浮窗,由于小程序平台机制的限制,悬浮窗这种交互形态是无法实现的。
以上是我总结的小程序平台交互设计的显著特点,这也是我们后面围绕小程序平台做交互设计、交互实践的一个很重要的起点。
轻芒的交互实践案例一:列表的实现
下面跟大家分享一些轻芒的案例,首先讲一个大家用得最多的:列表。列表为什么重要?理清楚列表的逻辑对于整个产品后续的实现、对于工程师更好的去理解业务和设计,是非常重要的一个环节。在传统的一般界面组件里,列表是最复杂的一个模块,它会有很多数据的联动,它的交互、数据传递、事件分发都会涉及到很多问题,这也是我们为什么要妥善思考列表怎么做。而且在很多场景里,长列表的性能是很多性能瓶颈的来源,这也是需要额外注意的地方。
什么叫列表?比如下图左边是非常显然的列表,可以无限加载、不断滚动的内容流,我们认为每一个卡片都是一个列表项。右图看上去是一个内容详情页,在实践中我们也把它做成了列表,我们会把它每个段落抽象出来,按照列表来渲染。
小程序里面最大的特色是,它并没有一个原生的列表组件。我们刚刚说 MVC,而 MVC 中最核心的是模型(Model)的设计,就是整个产品中的数据模型到底是什么样的,这是真正影响开发时间的。如果模型设计得好,就像种了一棵树,树上长着枝桠,如果你把所有的数据模型抽象的非常漂亮,那树上的枝桠,也就是 View 和 Controller,实现起来会非常轻松。如果你的模型完全不做抽象,随便收拾一下就开始做交互开发,那即便你在界面层选了很多漂亮的转型、用很多 fancy 的框架,你也很难把代码复杂度降下来,整个产品的复用度也不会高,这也是我为什么会说如果拿一个项目最好从列表入手,因为列表的数据模型通常是最复杂的,列表清楚了,整个产品也就清楚了。
在轻芒里,比如刚刚那个瀑布流,我们会抽象成一个 event 对象,当然,我们不是在所有的小程序开发里都这么做,因为在其他小程序中业务不同,抽象模型也可能不一样,我们配套的列表实现也会不一样。在轻芒杂志,因为有非常复杂的卡片类型或者不同类型的交互样式,我们会把不同类型的数据统一抽象成一个 event 对象,每个 event 会有一个类型,然后会围绕 event 来设计列表控件。具体的实现,我们用了微信的模板(Template)来做抽象和封装,这是因为我们这个项目做太早了,我们做的时候没有任何开源项目,只能够全部自己封装。
这里面最重要的其实是对 event 对象的抽象,也就是产品中最核心的数据模型,如果这个搞清楚了,你会发现后面东西会变得很简单。在这个代码中,每一个卡片的渲染,可能有两个不同的实现方式,有一些卡片是用模板实现的,比如说 single-card-*,在实现中,我们会直接用 single-card 加上具体的 type 来实现列表项,比如说 single-card-article、single-card-image、single-card-video 等等。在微信有了自定义组件之后,我们把一些后来实现的新卡片放成了自定义组件,比如这里的 universe card,我们把新的卡片用了新的方式来做。
早期因为我们用 Template 对界面元素做封装,这种封装包含了任何互动的实现。因为微信早期没有提供任何可封装机制,现在你也许可以通过不同的开源框架搞定,早期唯一的办法就是把一些和界面互动相关的函数抽象成一些 Mixin 模块,然后在页面配置里直接整合进去,增强复用度。这个方案我们现在依然在用,这也是在小程序里最常用的实践方案之一。
另一方面,封装列表也是为了统一相关联的界面组件,比如统一的 loading 样式,统一的空白页样式,翻页的逻辑和多级嵌套等。举例来说,所有数据的加载,在列表中定义了翻页方式,就定义了产品的客户端和服务端的交互方式。在轻芒,所有的服务端翻页都会使用 Next Url 模式,服务端会告诉客户端有没有下一页,有的话客户端滚到底就会加载下一页。这告诉我们,如果服务端具有统一的 API 模式,客户端开发会变得很轻松,抽象和复用可以做得更充分。当然,这反向也约束了服务端,需要服务端提供统一的或者整合的 API,这样落地到公司全部团队,可以提升整体的效能。对前端来讲,也只有这样的方案才是能够极大地简化开发复杂度。
列表实现中,还有一个和小程序特性特别相关的点,就是如何在列表中嵌入一个原生组件,比如我想在列表里播视频,我想在列表里嵌个地图、嵌个输入框,该怎么办?如果用微信原生组件的渲染机制,如果嵌一个视频在内容流里,它可能会遮住很多你想弹出来的对话框;有可能你想实现页面快速滚动,视频会有非常慢的拖影,这都很影响产品体验。
在轻芒杂志中,我们用过非常多的交互方案去尝试这个东西,最后是团队一起来解决的。这个解决方案的核心理念是:不要在列表中直接使用原生组件播放视频,而是用设计的方案去规避,基于分层的方案来实现整个交互。当原生组件出现的时候,整个交互停止,比如看左边这个视频,在播放一个嵌入的视频时,这个视频好像是「长」在那个卡片上的,其实它是个虚拟的定位,只是大概在那个位置。用户一旦滚动页面,视频立刻收起来不再播放。可能这听上去很不「技术」,但实际上这是你对平台的理解,你知道这是一个系统设计上的硬坑,如果平台不搞定这点,无论谁来做都会碰到同样的问题。与其这样,不如我们理解平台之后,和团队商量,换种方式实现,把事情变得更简单、更轻松。
列表实现中还有一些问题,就是如何处理数据和状态。每个列表项里面会包含很多元素,比如列表项里会有一篇文章的基本信息,同时右下角有个交互按钮,用户点击之后按钮状态发生变化,对轻芒来讲这就是「马克」,而这些都不属于原生数据,会随着用户的交互而变化,因此被称为状态。在早期小程序开发中,大家可能不太会区分状态和数据,会把数据和状态放到同一个数据模型上,因为实现逻辑非常复杂,在纯数据驱动的模式下会碰到非常多问题。
而轻芒的实践经验,就是要让数据和状态分离。比如拿到数据后,我们会把数据分成两部分,一部分是原生数据,它们不会随着用户交互而发生变化,用列表来进行存储;而另一部分是状态,它会随着用户交互发生变化,这里我们会用字典进行存储。图示的代码中, ui-switch 和 subscribed 的对象都存储了状态信息,当用户对特定元素进行操作后,只需要在控制层改变 switch 对应 id 的某一个值,就可以对界面进行快速的更新。这看上去是一个非常小的优化,但如果你在所有界面交互中都能把控好这一点,带来的收益是非常大的。它还有一个特点,是中心化。比如刚刚看到的视频播放,我们同时只允许一个视频播放,用户点了某个视频后其他视频会停止播放,这时候你只需要在控制层把整个状态清空,对交互对象的状态重新设定即可,这都是一些听上去比较细节,但从实践来讲非常重要的事情。
最后一个问题,是长列表性能优化,与其说是经验分享,不如说教训分享。在轻芒杂志中,我们会用到大量的长列表,无限滚动的瀑布流、非常长的文章等等。这些都会导致列表变长,带来卡顿。为什么?从小程序的渲染机制来看,从设定数据到界面渲染呈现,也就是调用 setData 函数到 setData 函数返回,这里面有两件可能比较耗时的事情,一个是数据比对:小程序需要找到有哪些数据更新了,这些数据关联了哪些界面元素。还有一件就是刷新界面:把更新的数据呈现出来,先在虚拟节点上做渲染,重新放到前端来呈现。而小程序最大的问题,在于这两个环节太「黑盒」了,它告诉开发者的信息非常少,因此优化起来就比较困难。我们知道轻芒杂志的渲染都花在这里了,所以会有卡顿,但到底是哪里耗了时间,其实我们不能知道。
于是我们做的性能优化方案就是试,不停地做二分,少做一部分逻辑,看看性能有没有好转。我们最早把性能点放在 setData 不能追加上,虽然 setData 可以修改一个列表中的元素,但它一旦要增删元素,就要全部替换,这是现在微信整个数据驱动模型的一个核心问题。在这里,我们尝试使用固定的列表项个数,先虚拟一百个列表项,可能只有十个有数据,如果新的元素需要追加,就只用修改而不需要全部替换。最后的结论是,有效果,但没太大的效果,而开发成本却增加了不少。后来我们把性能问题,更多地放在了渲染上,发现一个有效的方案是,让界面设计变简单一些,需要渲染的元素变少,性能就好了不少。所以在长列表中,很大的瓶颈还是在 DOM 渲染上,小程序并没有为列表提供一个元素回收机制,这导致它会完整渲染整个列表,它并不关心这个列表项需不需要在界面上呈现、需不需要给用户看,这就会造成很多性能问题。
在这里我们也没有太完整的经验,更多的是教训。那对于大家来说,如果你能预见到你的产品会有很长列表,你可以提前考虑是不是要降低列表项的设计复杂度,控制每个列表项 DOM 节点的数量,减少一些 DOM 节点上绑定的数据和事件等等,这些可能会对产品的性能有所优所。当然整体来看小程序的渲染还是个黑盒,我们能用的优化手段也比较少。总结一下,其实最想和大家聊的是交互的开发和业务是密不可分的,需要去和设计、后端、产品,一起去改进,在技术设计中更多的去理解需求。
轻芒的交互实践案例二:全局窗口的实现
我们知道微信小程序是一个单窗口的交互平台,那什么是全局窗口呢?比如,上面左边这个分享卡片,它需要在任何轻芒杂志的页面都能被呼出,从而对页面进行分享。右边是我们自定义的 toast,很多时候我们需要的 toast 和微信小程序官方提供的不一样,这时候就需要基于全局窗口来实现。但因为在小程序中没有全局窗口机制,对我们而言唯一的办法就是通过把组件放进每一个页面,随时可以呼出使用,模拟全局窗口,我们把这个称为全局组件。在这个部分,我会重点来聊组件的封装,在小程序中最好的组件方案是怎样的。
刚刚大家听了很多第三方的框架,在轻芒,我们主要还都是基于原生的机制来实现的。常见原生方式有两种:基于模板(Template) 和基于自定义组件的,这两种封装方式我们都很常用。像这个代码展示的,我们有的卡片使用了自定义组件来进行封装,有一些加载的进度条可能是模板来封装的。但如果我把它做成全局组件,让每个页面都包含,这两种方式都需要我在各个页面拷贝大量的重复代码,可能有上百行,一旦需要变更,就会需要同时修改整个小程序里几十个页面。这种方案我们早期使用过,但发现效果不好,维护起来太麻烦了。于是我们重新设计了一下,发现在小程序中还有一些更简单的封装方案更适合,就是大家今天可能很少用的 include 方案,就相当于直接引入一个别的代码块。这是一个非常传统的一个方案,我们现在往往会强调组件,强调封装,要封装得漂亮,要让组件边界变得干净,但实际上在这样的场景里,我们打破一些简单的组件的限制,反而会让事情变得简单。
我们会写一个全局窗口的文件叫 global.wxml,把所有小程序中会用到的全局组件的数据和实现都放在这里面,图上的代码是叠过的,其实展开还是比较长的,因为在 include 中不能再使用其他 Template 进行封装了。对应的,还配套一些 js 代码,我们封装了 action.js 的文件,里面会包括各种全局组件的控制,比如模态对话框、输入对话框、toast、分享卡片等等。在最后使用里面,每一个页面只要加两行,一个是 include global,一个 Mixin 这个 action.js,这解决了问题。
这个例子非常小,背后想聊的主要还是组件封装的思路。做技术设计,始终要考虑怎么封装组件、怎么复用,因为这个对所有前端开发、服务端开发都是重要的,如果不复用、不去尝试做一些抽象,那么长此以往代码会变得非常散,难以维护、难以阅读。但怎么做封装,怎么做实践?除了把组件抽象得非常漂亮外,也可以尝试用一些边界更模糊的封装方案,可以显著的让代码变短变简单,更易于变更和维护。比如,在前面例子中,我要变更某个全局组件的样式,只要修改 global.xml,而调用该文件的地方是不用改的,用开闭原则来看,它是个非常满足开闭原则的实现策略。
轻芒的交互实践案例三:马克
最后再跟大家分享一个案例,这是一个非常「轻芒」的交互实现策略。图上的这个交互叫「马克」。这个交互是轻芒杂志的一个核心功能,我们之前在 Android、iOS 上都做过类似的实现,最麻烦的确实是在小程序上。要实现这个,首先需要去理解小程序的事件机制。小程序在早期只有绑定,在后期开放了事件捕获流程,那就成了一个非常传统的事件传播机制,先是事件从父到子捕获,然后是从子到父的事件冒泡。所有你想提供的交互,不管是左右滑动、翻页、点击翻页等等,都依赖这个事件传播机制。
要实现这样的马克交互,首先不是处理事件,而是对数据模型进行抽象,由于小程序无法精确的知道元素的排版信息,我们最早是做不出来这个效果的,最后做了一个模型抽象,把一个文章的段落切分成了句子,每个句子用一个 View 来进行渲染。当你进行马克,需要选中一部分文字的时候,我们其实是直接让你选择一个句子,这样可以避免去考虑句子内的排版,从而非常好的实现了马克的效果,这也是和设计师围绕技术可行性反复讨论出来的,小程序没有办法控制 DOM 节点,更没有办法拿到排版里面的细节,从数据上解决这个问题,是唯一的思路,你可以去改变一些数据模型,为你后来的交互做一些准备。
在我们掉过很多坑,当用户移动手指去修改选中区域的时候,如果我们用了微信的条件判断 wx:if 去改变底色之类的,很可能会让整个控件树的结构发生变化,整个交互界面会陷入一个非常可怕的状态,完全失去响应,我们推测这是触发了微信底层的 bug。所以在这样的交互中,最好的方式是只改变 CSS 的,尽可能不去改变界面元素的结构。
这里有个细节,在触发了调整马克选中区域之后,我们再拖动手指的时候,整个界面是不滚动的,直到我们放开手指之后,整个界面才可以重新开始滚动。这时候就用到了我刚刚说过的事件传播机制的流程处理,在小程序中,只要你设定了捕获函数就必须捕获事件,不像其他平台,可以通过捕获函数的返回值来控制是否需要捕获,而如果不进行捕获,后续又没法中断整个页面的滚动。那怎么办?我们后来用了一些比较 Hack 的方式,我们去动态的修改绑定的捕获函数名。当需要捕获时,就把需要的函数设定上,而停止捕获时,就把捕获函数设置为空白字符串,这样可以绕过它的捕获机制。这时候,就可以控制界面在什么时候,可以滚动页面,什么时候只可以调整选中区域了。
这个马克交互我们改版过很多次,早期由于没有 query 任何 DOM 元素的机制,非常难以实现。而现在,微信提供了 query DOM 节点的 API,我可以大概知道 DOM 节点在哪里,现在我们会反复用到 query 函数,了解界面状态,来调整可以进行的交互。整个马克的实现过程中,工程师和设计师有非常多的讨论,设计师根据技术限制重新想一些交互方案,工程上不断地尝试采取一些类似 Hack 的手段去落地这些方案。如果你希望把交互做得更往前,毫无疑问也会需要这样进行设计和实现,也会踩到一些平台的坑,这些经验也可以供大家参考。
上面分享的三个案例,不全部是单纯的技术,有不少技术外的事情。交互,看上去是一个纯前端实现的问题,其实它的实现是整个团队的事情,包括产品、设计、以及后端的 APIs 设计等等。也是平台能力和产品设计的交互融合。
另外,在小程序这个平台上尤其需要重视数据模型的设计,一定要让整个数据模型理解好业务,把整个数据模型设计得清晰,什么是数据、什么是状态、哪些应该隔离、哪些应该放在一起,把这些事情区分好,可以让整个前端开发变得很轻松。此外,我们在处理原生组件时也要特别小心,需要规避掉一些设计方案,否则会带来一系列 Bug。
这是轻芒的实践,我相信和大家的具体的业务需求、目标会不完全一致。但也期望今天的分享可以给大家一些启发,如果大家能运用到其中的一些,对我们来讲就足够了。谢谢大家。
最后,为你推荐